use crate::GLItem;
use futures::{Future, IntoFuture, Stream};

const BASE_URI: &str = "https://edge.ldscdn.org/mobile/gospelstudy/production/v4/";

lazy_static::lazy_static! {
    static ref CACHE_DIR: std::path::PathBuf = {
        let mut result = dirs::cache_dir().expect("Failed to determine cache directory");
        result.push("oglq.vpzom");

        result
    };
}

#[derive(serde_derive::Deserialize, serde_derive::Serialize, Debug)]
struct CatalogIndexInfo {
    #[serde(rename = "catalogVersion")]
    catalog_version: u32,
}

pub struct CatalogProvider4 {
    http_client: hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
}

impl CatalogProvider4 {
    pub fn new() -> Self {
        let connector = hyper_rustls::HttpsConnector::new(2);
        Self {
            http_client: hyper::Client::builder().build(connector),
        }
    }

    fn load_catalog(&self) -> impl Future<Item = GLItem, Error = String> + Send {
        self.load_catalog_db().and_then(|conn| {
            blocking_future::BlockingFuture::new(move || {
                let conn = &conn.lock().unwrap();
                let entries = conn
                    .prepare("SELECT id, name FROM item_category ORDER BY id")
                    .and_then(|mut stmt| {
                        let res: Result<Vec<_>, _> = stmt
                            .query_map(rusqlite::NO_PARAMS, |row| {
                                Ok(GLItem::Link {
                                    title: row.get(1)?,
                                    uri: format!("/category/{}", row.get::<_, i64>(0)?),
                                })
                            })?
                            .collect();
                        res
                    })
                    .map_err(|err| format!("Failed to retrieve categories: {:?}", err))?;

                Ok(GLItem::Collection {
                    title: "Library".to_owned(),
                    items: entries,
                })
            })
            .map_err(|err| format!("Failed to retrieve categories: {:?}", err))
            .and_then(|x| x)
        })
    }

    fn load_catalog_db(
        &self,
    ) -> impl Future<Item = std::sync::Mutex<rusqlite::Connection>, Error = String> + Send {
        let http_client = self.http_client.clone();
        self.load_index()
            .and_then(|body| {
                let path = CACHE_DIR.join(format!("Catalog-{}.sqlite", body.catalog_version));
                tokio::fs::metadata(path.clone()).then(|res| match res {
                    Ok(_) => futures::future::Either::A(futures::future::ok(path)),
                    Err(err) => {
                        if err.kind() == std::io::ErrorKind::NotFound {
                            futures::future::Either::B(download_catalog(http_client))
                        } else {
                            futures::future::Either::A(futures::future::err(format!(
                                "Failed to open catalog file: {:?}",
                                err
                            )))
                        }
                    }
                })
            })
            .and_then(|path| {
                blocking_future::BlockingFuture::new(|| {
                    Ok(std::sync::Mutex::new(
                        rusqlite::Connection::open(path)
                            .map_err(|err| format!("Failed to open database: {:?}", err))?,
                    ))
                })
                .map_err(|err| format!("error in database open future: {:?}", err))
                .and_then(|x| x)
            })
    }

    fn load_index(&self) -> impl Future<Item = CatalogIndexInfo, Error = String> + Send {
        load_index(self.http_client.clone())
    }

    fn download_index(&self) -> impl Future<Item = CatalogIndexInfo, Error = String> + Send {
        download_index(&self.http_client)
    }

    fn load_category(&self, category_id: i64) -> impl Future<Item = GLItem, Error = String> + Send {
        self.load_catalog_db().and_then(move |conn| {
            blocking_future::BlockingFuture::new(move || {
                let conn = &conn.lock().unwrap();

                let title = conn
                    .query_row(
                        "SELECT name FROM item_category WHERE id=?",
                        &[category_id],
                        |row| row.get(0),
                    )
                    .map_err(|err| format!("Failed to find category: {:?}", err))?;

                let items = conn
                    .prepare(
                        "SELECT uri, title FROM item WHERE item_category_id=? AND language_id=?",
                    )
                    .and_then(|mut stmt| {
                        let res: Result<Vec<_>, _> = stmt
                            .query_map(
                                &[
                                    category_id,
                                    1, /* English, potentially to be improved later */
                                ],
                                |row| {
                                    Ok(GLItem::Link {
                                        title: row.get(1)?,
                                        uri: row.get(0)?,
                                    })
                                },
                            )?
                            .collect();
                        res
                    })
                    .map_err(|err| format!("Failed to list category contents: {:?}", err))?;

                Ok(GLItem::Collection { title, items })
            })
            .map_err(|err| format!("Error in query block: {:?}", err))
            .and_then(|x| x)
        })
    }
}

impl crate::CatalogProvider for CatalogProvider4 {
    fn load(&self, uri: &str) -> Box<Future<Item = GLItem, Error = String> + Send> {
        if uri == "/" {
            Box::new(self.load_catalog())
        } else if uri.starts_with("/category/") {
            let category = &uri[10..];
            let category = match category.parse::<i64>() {
                Ok(category) => category,
                Err(_) => return Box::new(futures::future::err("Invalid category ID".to_owned())),
            };
            Box::new(self.load_category(category))
        } else {
            let uri = uri.to_owned();
            let http_client = self.http_client.clone();
            Box::new(self.load_catalog_db()
                     .and_then({
                         let uri = uri.clone();
                         |conn| {
                             blocking_future::BlockingFuture::new(move || {
                                 let conn = &conn.lock().unwrap();
                                 let uri_path = match uri.find('#') {
                                     Some(idx) => &uri[..idx],
                                     None => &uri,
                                 };
                                 let split: Vec<_> = uri_path.split('/').collect();

                                 let uris: Vec<_> = (2..=split.len()).map(|count| split[..count].join("/")).collect();

                                 println!("{:?}", uris);

                                 let result: (String, i64) = conn.query_row(&format!("SELECT id, version FROM item WHERE uri IN ({}) AND language_id=1", vec!["?"; uris.len()].join(", ")), &uris, |row| {
                                     Ok((row.get(0)?, row.get(1)?))
                                 })
                                 .map_err(|err| format!("Failed to find item: {:?}", err))?;

                                 Ok(result)
                             })
                             .map_err(|err| format!("Error in database location block: {:?}", err))
                                 .and_then(|x| x)
                         }
                     })
                     .and_then(move |(id, version)| {
                         load_package_db(id, version, http_client)
                             .and_then(move |conn| {
                                 blocking_future::BlockingFuture::new(move || {
                                     let conn = &conn.lock().unwrap();
                                     match conn.query_row("SELECT title, id FROM nav_collection WHERE uri=?", &[&uri], |row| Ok((row.get(0)?, row.get::<_, i64>(1)?))) {
                                         Ok((collection_title, collection_id)) => {
                                             let items = conn.prepare("SELECT uri, title FROM (SELECT uri, title, position, nav_section_id FROM nav_item UNION ALL SELECT uri, title, position, nav_section_id FROM nav_collection) AS content WHERE EXISTS(SELECT 1 FROM nav_section WHERE id=nav_section_id AND nav_collection_id=?) ORDER BY position ASC")
                                                 .and_then(|mut stmt| {
                                                     let res: Result<Vec<_>, _> = stmt.query_map(&[collection_id], |row| {
                                                         Ok(GLItem::Link {
                                                             title: row.get(1)?,
                                                             uri: row.get(0)?,
                                                         })
                                                     })?.collect();

                                                     res
                                                 })
                                             .map_err(|err| format!("Failed to list collection contents: {:?}", err))?;
                                             Ok(GLItem::Collection { title: collection_title, items })
                                         },
                                         Err(rusqlite::Error::QueryReturnedNoRows) => {
                                             let (title, content): (_, String) = conn.query_row("SELECT title, (SELECT content_html FROM subitem_content WHERE subitem_id=subitem.id) AS content FROM subitem WHERE uri=?", &[uri], |row| Ok((row.get(0)?, row.get(1)?)))
                                                 .map_err(|err| format!("Failed to query for item content: {:?}", err))?;

                                             Ok(GLItem::Content { title, body: content })
                                         },
                                         Err(other) => Err(format!("Failed to query for item content: {:?}", other)),
                                     }
                                 })
                                 .map_err(|err| format!("Error in query block: {:?}", err))
                                     .and_then(|x| x)
                             })
                     }))
        }
    }
}

fn download_catalog(
    http_client: hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
) -> impl Future<Item = std::path::PathBuf, Error = String> + Send {
    load_index(http_client.clone())
        .and_then(move |body| {
            let catalog_version = body.catalog_version;
            let dest = CACHE_DIR.join(format!("{}.zip", catalog_version));
            format!("{}languages/eng/catalogs/{}.xz", BASE_URI, catalog_version)
                .parse()
                .map_err(|err| format!("Failed to construct URI: {:?}", err))
                .map(move |uri| {
                    println!("{:?}", uri);
                    http_client
                        .get(uri)
                        .map_err(|err| format!("Failed to send request: {:?}", err))
                        .map(move |x| (x, catalog_version))
                })
        })
        .and_then(|x| x)
        .and_then(|(resp, catalog_version)| {
            println!("{:?}", resp);
            blocking_future::BlockingFuture::new(|| {
                tempfile::tempfile().map_err(|err| format!("Failed to create temp file: {:?}", err))
            })
            .map_err(|err| format!("Failed to create temp file: {:?}", err))
            .and_then(|x| x)
            .and_then(|tmp_file| {
                resp.into_body()
                    .map_err(|err| format!("Failed to download file: {:?}", err))
                    .fold(tokio::fs::File::from_std(tmp_file), |file, chunk| {
                        tokio::io::write_all(file, chunk)
                            .map(|(file, _)| file)
                            .map_err(|err| format!("Failed to write downloaded file: {:?}", err))
                    })
            })
            .map(move |file| (file.into_std(), catalog_version))
        })
        .and_then(|(mut tmp_file, catalog_version)| {
            blocking_future::BlockingFuture::new(move || {
                let dest_file_path = CACHE_DIR.join(format!("Catalog-{}.sqlite", catalog_version));
                let mut dest_file = std::fs::File::create(&dest_file_path)
                    .map_err(|err| format!("Failed to open output file: {:?}", err))?;

                let mut decoder = xz2::write::XzDecoder::new(dest_file);

                std::io::Seek::seek(&mut tmp_file, std::io::SeekFrom::Start(0)).map_err(|err| format!("Failed to seek in tmp_file: {:?}", err))?;

                std::io::copy(&mut tmp_file, &mut decoder)
                    .map_err(|err| format!("Failed to extract archive: {:?}", err))?;

                Ok(dest_file_path)
            })
            .map_err(|err| format!("Failed to read archive: {:?}", err))
            .and_then(|x| x)
        })
}

fn load_index(
    http_client: hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
) -> impl Future<Item = CatalogIndexInfo, Error = String> + Send {
    let path = CACHE_DIR.join("index.json");
    tokio::fs::read(path.clone())
        .then(|res| match res {
            Ok(body) => serde_json::from_slice(&body)
                .map_err(|err| format!("Failed to parse index cache: {:?}", err))
                .map(Some),
            Err(err) => {
                if err.kind() == std::io::ErrorKind::NotFound {
                    Ok(None)
                } else {
                    Err(format!("Failed to read index cache: {:?}", err))
                }
            }
        })
        .and_then(move |result| match result {
            Some(info) => futures::future::Either::A(futures::future::ok(info)),
            None => {
                let cache_dir: &std::path::Path = &CACHE_DIR;
                futures::future::Either::B(
                    download_index(&http_client)
                        .join(
                            tokio::fs::create_dir_all(cache_dir).map_err(|err| {
                                format!("Failed to create cache directory: {:?}", err)
                            }),
                        )
                        .and_then(|(info, _)| {
                            println!("{:?}", info);
                            serde_json::to_vec(&info)
                                .map_err(|err| format!("Failed to reserialize info: {:?}", err))
                                .into_future()
                                .and_then(|body| {
                                    tokio::fs::File::create(path)
                                        .and_then(|file| {
                                            tokio::io::write_all(file, body).map(|_| info)
                                        })
                                        .map_err(|err| {
                                            format!("Failed to write index cache: {:?}", err)
                                        })
                                })
                        }),
                )
            }
        })
}

fn download_index(
    http_client: &hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
) -> impl Future<Item = CatalogIndexInfo, Error = String> + Send {
    format!("{}/languages/eng/index.json", BASE_URI)
        .parse()
        .map_err(|err| format!("Failed to construct URI: {:?}", err))
        .map(|uri| {
            http_client
                .get(uri)
                .map_err(|err| format!("Failed to send request: {:?}", err))
        })
        .into_future()
        .and_then(|x| x)
        .and_then(|resp| {
            resp.into_body()
                .concat2()
                .map_err(|err| format!("Failed to read response: {:?}", err))
        })
        .and_then(|body| {
            serde_json::from_slice(&body)
                .map_err(|err| format!("Failed to parse response: {:?}", err))
        })
}

fn load_package_db(
    package_id: String,
    version: i64,
    http_client: hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
) -> impl Future<Item = std::sync::Mutex<rusqlite::Connection>, Error = String> + Send {
    let path = CACHE_DIR
        .join("packages")
        .join(format!("{}-{}.sqlite", package_id, version));
    tokio::fs::metadata(path.clone())
        .then(move |res| match res {
            Ok(_) => futures::future::Either::A(futures::future::ok(path)),
            Err(err) => {
                if err.kind() == std::io::ErrorKind::NotFound {
                    futures::future::Either::B(download_package_db(
                        &package_id,
                        version,
                        &http_client,
                    ))
                } else {
                    futures::future::Either::A(futures::future::err(format!(
                        "Failed to open package file: {:?}",
                        err
                    )))
                }
            }
        })
        .and_then(|path| {
            blocking_future::BlockingFuture::new(|| {
                Ok(std::sync::Mutex::new(
                    rusqlite::Connection::open(path)
                        .map_err(|err| format!("Failed to open database: {:?}", err))?,
                ))
            })
            .map_err(|err| format!("error in database open future: {:?}", err))
            .and_then(|x| x)
        })
}

fn download_package_db(
    package_id: &str,
    version: i64,
    http_client: &hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
) -> impl Future<Item = std::path::PathBuf, Error = String> + Send {
    let package_id = package_id.to_owned();
    format!("{}/languages/eng/item-packages/{}/{}.xz", BASE_URI, package_id, version)
        .parse()
        .map_err(|err| format!("Failed to construct URI: {:?}", err))
        .map(|uri| {
            http_client
                .get(uri)
                .map_err(|err| format!("Failed to send request: {:?}", err))
        })
        .into_future()
        .and_then(|x| x)
        .and_then(|resp| {
            blocking_future::BlockingFuture::new(|| {
                tempfile::tempfile().map_err(|err| format!("Failed to create temp file: {:?}", err))
            })
            .map_err(|err| format!("Failed to create temp file: {:?}", err))
            .and_then(|x| x)
            .and_then(|tmp_file| {
                resp.into_body()
                    .map_err(|err| format!("Failed to download file: {:?}", err))
                    .fold(tokio::fs::File::from_std(tmp_file), |file, chunk| {
                        tokio::io::write_all(file, chunk)
                            .map(|(file, _)| file)
                            .map_err(|err| format!("Failed to write downloaded file: {:?}", err))
                    })
            })
            .map(move |file| file.into_std())
        })
        .and_then(move |mut tmp_file| {
            blocking_future::BlockingFuture::new(move || -> Result<std::path::PathBuf, String> {
                let dest_file_parent = CACHE_DIR.join("packages");
                std::fs::create_dir_all(&dest_file_parent)
                    .map_err(|err| format!("Failed to create packages directory: {:?}", err))?;
                let dest_file_path =
                    dest_file_parent.join(format!("{}-{}.sqlite", package_id, version));
                let mut dest_file = std::fs::File::create(&dest_file_path)
                    .map_err(|err| format!("Failed to open output file: {:?}", err))?;

                let mut decoder = xz2::write::XzDecoder::new(dest_file);

                std::io::Seek::seek(&mut tmp_file, std::io::SeekFrom::Start(0)).map_err(|err| format!("Failed to seek in tmp_file: {:?}", err))?;

                std::io::copy(&mut tmp_file, &mut decoder)
                    .map_err(|err| format!("Failed to extract package: {:?}", err))?;

                Ok(dest_file_path)
            })
            .map_err(|err| format!("Failed to read archive: {:?}", err))
            .and_then(|x| x)
        })
}
